# -*- coding: utf-8 -*-
#
# geXtras plugin for gedit
#
# See README for info or go to http://code.google.com/p/gextras/
#
# Copyright (c) 2010-2011, Simon Downes <simon@simondownes.co.uk>
#
# Licensed under the MIT License;
# A copy of the license is included in the LICENSE file accompanying this
# distribution or online at http://www.opensource.org/licenses/mit-license.php

import random

import gtk
import gedit

import buffer_tools
import bookmarks
from pair_completer import PairCompleter

ui_str = """<ui>
   <menubar name="MenuBar">
      <menu name="EditMenu" action="Edit">
         <placeholder name="EditOps_3">
            <menuitem name="geXtrasSelectLine" action="geXtrasSelectLine"/>
            <separator/>
            <menuitem name="geXtrasUppercase" action="geXtrasUppercase"/>
            <menuitem name="geXtrasLowercase" action="geXtrasLowercase"/>
            <menu name="geXtrasOtherCase" action="geXtrasOtherCase">
               <menuitem name="geXtrasCapitalise" action="geXtrasCapitalise"/>
               <menuitem name="geXtrasSentenceCase" action="geXtrasSentenceCase"/>
               <menuitem name="geXtrasInvertCase" action="geXtrasInvertCase"/>
            </menu>
            <separator/>
            <menuitem name="geXtrasComment" action="geXtrasComment"/>
            <menuitem name="geXtrasUncomment" action="geXtrasUncomment"/>
            <separator/>
            <menuitem name="geXtrasToggleBookmark" action="geXtrasToggleBookmark"/>
            <menuitem name="geXtrasPreviousBookmark" action="geXtrasPreviousBookmark"/>
            <menuitem name="geXtrasNextBookmark" action="geXtrasNextBookmark"/>
            <separator/>
         </placeholder>
      </menu>
      <menu name="ViewMenu" action="View">
         <placeholder name="gExtraViewOps">
         </placeholder>
      </menu>
      <menu name="DocumentsMenu" action="Documents">
         <placeholder name="DocumentsOps_1">
            <separator/>
            <menuitem name="geXtrasPreviousDocument" action="geXtrasPreviousDocument"/>
            <menuitem name="geXtrasNextDocument" action="geXtrasNextDocument"/>
         </placeholder>
      </menu>
   </menubar>
</ui>
"""

class geXtrasWindowHelper( object ):
   
   KeyPressHandlerName   = 'gextras_keypress_handler'
   TabChangedHandlerName = 'gextras_tab_changed_handler'
   TabAddedHandlerName   = 'gextras_tab_added_handler'
   WindowIDName          = 'gextras_window_id'
   
   def __init__( self, plugin, window ):
      
      print 'Attaching %s to' % plugin.name, window
      
      # generate a unique key for this window so we can update handlers if a view/tab moves to a new window
      random.seed()
      self._window_id = "hash value: %016x" % random.getrandbits(128)
      
      self._plugin    = plugin
      self._window    = window
      self._pc        = PairCompleter()
      self._bookmarks = bookmarks.BookmarkManager()
      
      self._add_ui()
      #self.update_ui()
      
   def deactivate( self ):
      
      print 'Detaching %s from' % self._plugin.name, self._window
      
      # disconnect handlers for each view in the window
      for view in self._window.get_views():
         self._disconnect_view_handlers(view)
      
      self._remove_ui()
      
      self._window = None
      self._plugin = None
      self._pc     = None
   
   def update_ui( self ):
      
      view = self._window.get_active_view()
      doc  = self._window.get_active_document()
      
      self._action_group.set_sensitive(doc != None)
      
      # make sure we're handling the keypress events for the current view
      if isinstance(view, gedit.View) and doc:
         
         # if the view changed windows then disconnect the existing handlers
         window_id = getattr(view, self.WindowIDName, None)
         if window_id and window_id != self._window_id:
            self._disconnect_view_handlers(view)
         
         setattr(view, self.WindowIDName, self._window_id)
         self._connect_view_handlers(view, doc)
   
   def _add_ui( self ):
      
      # get the UI manager
      manager = self._window.get_ui_manager()
      
      self._action_group = gtk.ActionGroup("geXtrasPluginActions")
      self._action_group.add_actions([
         
         # edit menu
         ("geXtrasSelectLine", None, 'Select L_ine', '<Control><Shift>a', 'Select the current line', self.select_line),
         ("geXtrasUppercase", None, 'Make _Uppercase', '<Control>u', 'Make current selection uppercase', self.change_case),
         ("geXtrasLowercase", None, 'Make _Lowercase', '<Control>l', 'Make current selection lowercase', self.change_case),
         ("geXtrasOtherCase", None, '_Other Case', None, 'Other case options', None),
            ("geXtrasCapitalise", None, 'Capitalise', None, 'Capitalise first letters of words in selection', self.change_case),
            ("geXtrasSentenceCase", None, 'Sentence Case', None, 'Capitalise first letters of first words of sentences in selection', self.change_case),
            ("geXtrasInvertCase", None, 'Invert Case', None, 'Invert case of selection', self.change_case),
         ("geXtrasComment", None, 'Co_mment', '<Control>m', 'Comment the current selection', self.comment_selection),
         ("geXtrasUncomment", None, 'U_n-comment', '<Control><Shift>m', 'Un-comment the current selection', self.comment_selection),
         ("geXtrasToggleBookmark", None, 'Toggle _Bookmark', '<Control>e', 'Toggle a bookmark on the current line', self.toggle_bookmark),
         ("geXtrasPreviousBookmark", None, 'Previous Bookmark', '<Control>comma', 'Scroll to the previous bookmark', self.empty_action),
         ("geXtrasNextBookmark", None, 'Next Bookmark', '<Control>period', 'Scroll to the next bookmark', self.empty_action),
         
         # documents menu
         ("geXtrasPreviousDocument", None, '_Previous Document', '<Control><Shift>Tab', 'Select previous document', self.tab_switch),
         ("geXtrasNextDocument", None, '_Next Document', '<Control>Tab', 'Select next document', self.tab_switch),
         
         # shortcuts only, no menu items
         ("geXtrasScrollUp", None, 'Scroll Up', '<Control>Up', 'Scroll up one line', self.scroll_line),
         ("geXtrasScrollDown", None, 'Scroll Down', '<Control>Down', 'Scroll down one line', self.scroll_line),
         
         #("", None, '', None, '', self._empty_action)
         #(Name, None, Label, Accelerator, Tooltip, Callback)
         
      ])
      
      # insert our cool stuff
      manager.insert_action_group(self._action_group, -1)
      self._ui_id = manager.add_ui_from_string(ui_str)
      
      # hide existing next/previous document items
      for k in ['DocumentsMenu/DocumentsNextDocument', 'DocumentsMenu/DocumentsPreviousDocument']:
         item = manager.get_widget('/MenuBar/' + k)
         if isinstance(item, gtk.Widget):
            item.hide()
      
      # display pretty icons for some menu items
      icons = {
         'EditMenu': {
            'geXtrasToggleBookmark'  : 'bookmark-new.svg',
            'geXtrasPreviousBookmark': 'bullet_left.png',
            'geXtrasNextBookmark'    : 'bullet_right.png'
         },
         'DocumentsMenu': {
            'geXtrasPreviousDocument': 'page_back.png',
            'geXtrasNextDocument'    : 'page_forward.png'
         }
      }
      for k in icons.keys():
         for item in manager.get_widget('/MenuBar/' + k).get_submenu().get_children():
            if item.get_name() in icons[k]:
               icon = gtk.Image()
               icon.set_from_file(self._plugin.path + '/icons/' + icons[k][item.get_name()])
               item.set_image(icon)
      
      manager.ensure_update()
      
      #for action_group in manager.get_action_groups():
      #   print action_group.get_name()
      #   for action in action_group.list_actions():
      #      print ' |-', action.get_name()
      
      #print manager.get_ui()
      
      # create bookmark pane
      self._bookmark_list = gtk.TreeView()
      
      cell = gtk.CellRendererText()
      column = gtk.TreeViewColumn('Line')
      column.pack_start(cell, True)
      column.add_attribute(cell, 'text', 0)
      self._bookmark_list.append_column(column)
      
      cell = gtk.CellRendererText()
      column = gtk.TreeViewColumn('Source')
      column.pack_start(cell, True)
      column.add_attribute(cell, 'text', 1)
      self._bookmark_list.append_column(column)
      
      self._bookmark_list.set_enable_tree_lines(True)
      self._bookmark_list.set_search_column(1)
      self._bookmark_list.set_rules_hint(True)
      self._bookmark_list.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_BOTH)
      
      pane = gtk.ScrolledWindow()
      pane.add(self._bookmark_list);
      self._bookmark_list.show()

      # install bookmark panel into bottom panel
      icon = gtk.Image()
      icon.set_from_file(self._plugin.path + '/icons/' + icons['EditMenu']['geXtrasToggleBookmark'])
      panel = self._window.get_bottom_panel()
      panel.add_item(pane, 'Bookmarks', icon)
      
      self._connect_window_handlers()
      
   def _remove_ui( self ):
      
      manager = self._window.get_ui_manager()
      
      # restore original next/previous document items
      item = manager.get_widget('/MenuBar/DocumentsMenu/DocumentsNextDocument')
      if isinstance(item, gtk.Widget):
         item.show()
      
      item = manager.get_widget('/MenuBar/DocumentsMenu/DocumentsPreviousDocument')
      if isinstance(item, gtk.Widget):
         item.show()
      
      # remove the junk we inserted and make sure the UI updates
      manager.remove_ui(self._ui_id)
      manager.remove_action_group(self._action_group)
      manager.ensure_update()
   
      # remove bookmark pane
      panel = self._window.get_bottom_panel()
      panel.remove_item(self._bookmark_list.parent)
      del(self._bookmark_list)
      
      self._disconnect_window_handlers()
      
   def _connect_window_handlers( self ):
      if getattr(self._window, self.TabChangedHandlerName, None) is None:
         handler_id = self._window.connect('active-tab-changed', self.on_tab_changed)
         setattr(self._window, self.TabChangedHandlerName, handler_id)
      
      if getattr(self._window, self.TabAddedHandlerName, None) is None:
         handler_id = self._window.connect('tab-added', self.on_tab_added)
         setattr(self._window, self.TabAddedHandlerName, handler_id)
   
   def _disconnect_window_handlers( self ):
      handler_id = getattr(self._window, self.TabChangedHandlerName, None)
      if handler_id is not None:
         self._window.disconnect(handler_id)
         setattr(self._window, self.TabChangedHandlerName, None)
      
      handler_id = getattr(self._window, self.TabAddedHandlerName, None)
      if handler_id is not None:
         self._window.disconnect(handler_id)
         setattr(self._window, self.TabAddedHandlerName, None)

   def _connect_view_handlers( self, view, doc ):
      if getattr(view, self.KeyPressHandlerName, None) is None:
         handler_id = view.connect('key-press-event', self.on_key_press, doc)
         setattr(view, self.KeyPressHandlerName, handler_id)
   
   def _disconnect_view_handlers( self, view ):
      handler_id = getattr(view, self.KeyPressHandlerName, None)
      if handler_id is not None:
         view.disconnect(handler_id)
         setattr(view, self.KeyPressHandlerName, None)
   
   def on_tab_added( self, window, tab ):
      doc = tab.get_document()
      doc.connect("loaded", self.on_doc_loaded)
   
   def on_tab_changed( self, window, tab ):
      doc = tab.get_document()
      if doc:
         uri = doc.get_uri()
         self._bookmark_list.set_model( self._bookmarks.get_store(uri) )

   def on_doc_loaded( self, doc, arg ):
      doc.connect("insert-text", self.on_insert_text)
      doc.connect("delete-range", self.on_delete_text)
      doc.connect("changed", self.on_doc_changed)

   def on_insert_text( self, doc, iter, text, length ):
      pass
   
   def on_delete_text(self, doc, start, end):
      pass
   
   def on_doc_changed(self, doc):
      uri = doc.get_uri()
      if uri:
         # what line was changed?
         start = doc.get_iter_at_mark(doc.get_insert())
         line  = start.get_line() + 1
         
         # if the line is a bookmark then update the bookmark caption
         store = self._bookmarks.get_store(uri)
         if store.exists(line):
            start.set_line_offset(0)
            end = start.copy()
            if not end.ends_line():
               end.forward_to_line_end()
            
            store.update( line, doc.get_text(start, end).strip() )
   
   def on_key_press( self, view, event, doc ):
      
      # didn't do nuffink guv
      handled = False;
      
      # get a nice unicode character
      char = unichr(gtk.gdk.keyval_to_unicode(event.keyval))
      
      # if we got a null char use None instead
      if char == '\0':
         char = None
      
      # ignore modifiers except ctrl, alt and shift
      state = event.state & gtk.accelerator_get_default_mod_mask()
      
      # which modifier combo did we have
      is_alt        = state == gtk.gdk.MOD1_MASK
      is_ctrl       = state == gtk.gdk.CONTROL_MASK
      is_ctrl_shift = state == gtk.gdk.CONTROL_MASK | gtk.gdk.SHIFT_MASK
      
      #print gtk.gdk.keyval_name(event.keyval)
      
      if is_ctrl:
         # ctrl + tab - next document
         if event.keyval in [gtk.keysyms.Tab, gtk.keysyms.ISO_Left_Tab]:
            handled = self.tab_switch( self._action_group.get_action('geXtrasNextDocument') )
         
         # ctrl + up - scroll one line
         elif event.keyval == gtk.keysyms.Up:
            handled = self.scroll_line( self._action_group.get_action('geXtrasScrollUp') )
         
         # ctrl + down - scroll one line
         elif event.keyval == gtk.keysyms.Down:
            handled = self.scroll_line( self._action_group.get_action('geXtrasScrollDown') )
         
         # ctrl + left - move cursor left one word
         elif event.keyval == gtk.keysyms.Left:
            buffer_tools.backward_word(doc, buffer_tools.WORD_MOVE)
            view.scroll_mark_onscreen(doc.get_insert())
            handled = True
         
         # ctrl + right - move cursor right one word
         elif event.keyval == gtk.keysyms.Right:
            buffer_tools.forward_word(doc, buffer_tools.WORD_MOVE)
            view.scroll_mark_onscreen(doc.get_insert())
            handled = True
         
         # ctrl + backspace - delete to start/end of word
         elif event.keyval == gtk.keysyms.BackSpace:
            buffer_tools.backward_word(doc, buffer_tools.WORD_DELETE)
            view.scroll_mark_onscreen(doc.get_insert())
            handled = True
         
         # ctrl + delete - delete to start/end of word
         elif event.keyval == gtk.keysyms.Delete:
            buffer_tools.forward_word(doc, buffer_tools.WORD_DELETE)
            handled = True
         
         # ctrl + x/c - cut/copy current line to clipboard if no selection
         elif event.keyval in [gtk.keysyms.x, gtk.keysyms.c]:
            handled = self.cut_copy_line(view, doc, event.keyval)
         
      elif is_ctrl_shift:
         # ctrl + shift + tab - previous document
         if event.keyval in [gtk.keysyms.Tab, gtk.keysyms.ISO_Left_Tab]:
            handled = self.tab_switch( self._action_group.get_action('geXtrasPreviousDocument') )
         
         # ctrl + shift + left - move cursor left one word
         elif event.keyval == gtk.keysyms.Left:
            buffer_tools.backward_word(doc, buffer_tools.WORD_SELECT)
            handled = True
         
         # ctrl + shift + right - move cursor right one word
         elif event.keyval == gtk.keysyms.Right:
            buffer_tools.forward_word(doc, buffer_tools.WORD_SELECT)
            handled = True
         
         # ctrl + shift + A - select current line
         elif event.keyval == gtk.keysyms.A:
            handled = self.select_line( self._action_group.get_action('geXtrasSelectLine') )
         
      elif is_alt:
         pass
      
      elif event.keyval == gtk.keysyms.BackSpace:
         handled = self._pc.delete(doc)
      
      elif char is not None:
         handled = self._pc.complete(doc, char)
      
      return handled
   
   def empty_action( self, action ):
      print action.get_name()
      return True
   
   def tab_switch( self, action ):
      
      active_tab = self._window.get_active_tab()
      all_tabs   = active_tab.parent.get_children()
      num_tabs   = len(all_tabs)
      
      i = 0
      for tab in all_tabs:
          i += 1
          if tab == active_tab:
              break
      
      if action.get_name() == 'geXtrasPreviousDocument':
          i -= 2
      
      if i < 0:
          tab = all_tabs[num_tabs - 1]
      elif i >= num_tabs:
          tab = all_tabs[0]
      else:
          tab = all_tabs[i]
      
      self._window.set_active_tab(tab)
      
      return True
   
   def select_line( self, action ):
      
      doc = self._window.get_active_document()
      
      if doc:
         buffer_tools.select_line(doc)
         
      return True
   
   def cut_copy_line( self, view, doc, key ):
      
      # if there is a selection then let the normal cut/copy functions handle it
      if doc.get_has_selection():
         return False
      
      buffer_tools.copy_line(doc, view.get_clipboard(gtk.gdk.SELECTION_CLIPBOARD), key == gtk.keysyms.x)
      
      return True
   
   def scroll_line( self, action ):
      
      view = self._window.get_active_view()
      doc  = self._window.get_active_document()
      
      if view and doc:
         
         for method in dir(view):
            print method
         
         # get the position of the cursor on the current line
         current_iter = doc.get_iter_at_mark(doc.get_insert())
         col = current_iter.get_line_offset()
         
         # get the character position at the top of the visible area
         viewport = view.get_visible_rect()
         
         if action.get_name() == 'geXtrasScrollUp':
            text_iter = view.get_iter_at_location(viewport.x, viewport.y)
            text_iter.backward_line()
         else:
            # don't need to call forward_line() as y + height already gives us the next line just below the current viewport
            text_iter = view.get_iter_at_location(viewport.x, viewport.y + viewport.height)
         
         view.scroll_to_iter(text_iter, 0.0, False)
         
         # make sure the cursor is on the screen, otherwise it can scroll off the bottom
         view.place_cursor_onscreen()
         
         # get the new cursor position and move it to the previous column position
         # or the end of the line if line is too short
         current_iter = doc.get_iter_at_mark(doc.get_insert())
         max_col = current_iter.get_chars_in_line() - 1
         if col < max_col:
            current_iter.set_line_offset(col)
         else:
            current_iter.set_line_offset(max_col)
         
         doc.place_cursor(current_iter)
      
      return True
   
   def change_case( self, action ):
      
      doc = self._window.get_active_document()
      
      if doc:
         action_name = action.get_name()
         if action_name == 'geXtrasUppercase':
            buffer_tools.change_case(doc, buffer_tools.CASE_UPPER)
         elif action_name == 'geXtrasLowercase':
            buffer_tools.change_case(doc, buffer_tools.CASE_LOWER)
         elif action_name == 'geXtrasCapitalise':
            buffer_tools.change_case(doc, buffer_tools.CASE_CAPITALISE)
         elif action_name == 'geXtrasSentenceCase':
            buffer_tools.change_case(doc, buffer_tools.CASE_SENTENCE)
         elif action_name == 'geXtrasInvertCase':
            buffer_tools.change_case(doc, buffer_tools.CASE_INVERT)
         
      return True
   
   def comment_selection( self, action ):
      
      doc  = self._window.get_active_document()
      view = self._window.get_active_view()
      
      if doc and view:
         
         lang = doc.get_language()
         if lang is None:
             return True
         
         comment_tag = lang.get_metadata('line-comment-start')
         if not comment_tag:
             return True
         
         if view.get_insert_spaces_instead_of_tabs():
            tab_char = ' '
         else:
            tab_char = "\t"
         
         if action.get_name() == 'geXtrasComment':
            buffer_tools.comment_selection(doc, comment_tag, tab_char)
         else:
            buffer_tools.uncomment_selection(doc, comment_tag)
         
      return True
   
   def toggle_bookmark( self, action ):
      
      doc  = self._window.get_active_document()
      
      if doc:
         start = doc.get_iter_at_mark(doc.get_insert())
         start.set_line_offset(0)
         end = start.copy()
         if not end.ends_line():
            end.forward_to_line_end()
         
         uri  = doc.get_uri()
         line = start.get_line() + 1
         text = doc.get_text(start, end).strip()
         
         self._bookmarks.get_store(uri).toggle(line, text)
         
         buffer_tools.highlight_line(doc,start.get_line(),True);
      